/*******************************************************************************
 * Copyright (c) 2016 Pivotal Software, Inc. and others 
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * 
 * The Eclipse Public License is available at 
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * and the Apache License v2.0 is available at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * You may elect to redistribute this code under either of these licenses.
 *  
 *  Contributors:
 *     Pivotal Software, Inc. - initial API and implementation
 ********************************************************************************/
package org.eclipse.cft.server.core.internal.client;

import org.eclipse.cft.server.core.internal.CFLoginHandler;
import org.eclipse.cft.server.core.internal.CloudErrorUtil;
import org.eclipse.cft.server.core.internal.CloudFoundryPlugin;
import org.eclipse.cft.server.core.internal.CloudFoundryServer;
import org.eclipse.cft.server.core.internal.Messages;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.server.core.IServer;
import org.eclipse.wst.server.core.internal.Server;

/**
 * Runs a request to the Cloud Foundry target connected to by a
 * {@link CloudFoundryServer}.
 * <p/>
 * This is the primary way of run any cloud requests that require a
 * CloudFoundryServer, as it performs various checks like ensuring that the
 * cloud Server has the right state, is connected, and that authorisation is
 * valid.
 * <p/>
 * This is intended to work with the "wrapper" {@link CFClient} which abstracts
 * which underlying client is being used (e.g. v1 or v2, or a hybrid)
 * 
 * 
 */
public abstract class CloudServerRequest<T> {

	protected final String requestLabel;

	private CloudFoundryServer cloudServer;

	private CFClient client;

	public CloudServerRequest(CloudFoundryServer cloudServer, CFClient client, String label) {
		Assert.isNotNull(label);
		Assert.isNotNull(cloudServer);
		Assert.isNotNull(client);
		this.cloudServer = cloudServer;
		this.requestLabel = generateRequestLabel(label, cloudServer);
		this.client = client;
	}

	/**
	 * Performs the client request, and if necessary, re-attempts the request
	 * after a certain interval IFF an error occurs based on
	 * {@link #getRetryTimeout()} and
	 * {@link #getRetryInterval(Throwable, SubMonitor)}.
	 * <p/>
	 * The default behaviour is to only attempt a client request once and quit
	 * after an error is encountered. Subclasses may modify this behaviour by
	 * overriding {@link #getRetryTimeout()} and
	 * {@link #getRetryInterval(Throwable, SubMonitor)}
	 * <p/>
	 * Note that reattempts are only decided based on errors thrown by the
	 * client invocation, not by results generated by the client invocation.
	 * @param client client whose operations are invoked. Never null.
	 * @param subProgress
	 * @return result of operation. Can be null.
	 * @throws CoreException if fatal error occurred while performing the
	 * operation (i.e. error that causes the operation to no longer be
	 * reattempted)
	 * @throws OperationCanceledException if further attempts are cancelled even
	 * if time still remains for additional attempts.
	 */
	protected T runRequestWithReattempts(CFClient client, IProgressMonitor monitor)
			throws CoreException, OperationCanceledException {
		Throwable error = null;

		boolean reattempt = true;
		long timeLeft = getRetryTimeout();

		// Either this operation returns a result during the waiting period or
		// an error occurred, and error
		// gets thrown

		while (reattempt) {

			long interval = -1;

			try {
				return runRequest(client, monitor);
			}
			catch (Throwable e) {
				error = e;
			}

			interval = getRetryInterval(error, monitor);
			timeLeft -= interval;
			reattempt = !monitor.isCanceled() && timeLeft >= 0 && interval > 0;
			if (reattempt) {

				try {
					Thread.sleep(interval);
				}
				catch (InterruptedException e) {
					// Ignore, continue with the next iteration
				}
			}
		}

		if (monitor.isCanceled()) {
			// check for cancel here, if specialized requests do not do it
			throw new OperationCanceledException(Messages.bind(Messages.OPERATION_CANCELED, requestLabel));
		}
		if (error instanceof OperationCanceledException) {
			throw (OperationCanceledException) error;
		}

		throw getErrorOnLastFailedAttempt(error);
	}

	protected CloudFoundryServer getCloudServer() {
		return cloudServer;
	}

	/**
	 * 
	 * @return result of client operation
	 * @throws CoreException if failure occurred while attempting to execute the
	 * client operation.
	 */
	public T run(IProgressMonitor monitor) throws CoreException {

		SubMonitor subProgress = SubMonitor.convert(monitor);
		subProgress.subTask(requestLabel);

		try {
			return promptCredentialsAndRun(subProgress);
		}
		catch (CoreException ce) {
			// See if it is a connection error. If so, parse it into readable
			// form.
			String connectionError = CloudErrorUtil.getConnectionError(ce);
			if (connectionError != null) {
				throw CloudErrorUtil.asCoreException(connectionError, ce, true);
			}
			else {
				throw ce;
			}
		}
		finally {
			subProgress.done();
		}

	}

	protected T checkClientConnectionAndRun(CFClient client, IProgressMonitor monitor) throws CoreException {

		try {
			return runRequestWithReattempts(client, monitor);
		}
		catch (CoreException ce) {

			CloudFoundryServer server = getCloudServer();

			if (server != null) {
				CFLoginHandler handler = new CFLoginHandler(client, server);

				CoreException accessError = null;
				String accessErrorMessage = null;

				if (handler.shouldAttemptClientLogin(ce)) {
					CloudFoundryPlugin.logWarning(NLS.bind(Messages.ClientRequest_RETRY_REQUEST, requestLabel));
					accessError = ce;

					int attempts = 3;
					String token = handler.login(monitor, attempts, CloudOperationsConstants.LOGIN_INTERVAL);
					if (token == null) {
						accessErrorMessage = Messages.ClientRequest_NO_TOKEN;
					}

					else {
						try {
							return runRequestWithReattempts(client, monitor);
						}
						catch (CoreException e) {
							accessError = e;
						}
					}
				}

				if (accessError != null) {
					Throwable cause = accessError.getCause() != null ? accessError.getCause() : accessError;
					if (accessErrorMessage == null) {
						accessErrorMessage = accessError.getMessage();
					}
					accessErrorMessage = NLS.bind(Messages.ClientRequest_SECOND_ATTEMPT_FAILED, requestLabel,
							accessErrorMessage);

					throw CloudErrorUtil.toCoreException(accessErrorMessage, cause);
				}
			}

			throw ce;
		}

	}

	/**
	 * Given an error, determine how long the operation should wait before
	 * trying again before timeout is reached. In order for attempt to be tried
	 * again, value must be positive. Any value less than or equal to 0 will not
	 * result in further attempts.
	 * 
	 * <p/>
	 * 
	 * By default it returns -1, meaning that the request is attempted only
	 * once, and any exceptions thrown will not result in reattempts. Subclasses
	 * can override to determine different reattempt conditions.
	 * @param exception to determine how long to wait until another attempt is
	 * made to run the operation. Note that if total timeout time is shorter
	 * than the interval, no further attempts will be made.
	 * @param monitor
	 * @return interval value greater than 0 if attempt is to be made . Any
	 * other value equal or less than 0 will result in the operation terminating
	 * without further reattempts.
	 * @throw CoreException if failed to determine interval. A CoreException
	 * will result in no further attempts.
	 */
	protected long getRetryInterval(Throwable exception, IProgressMonitor monitor) throws CoreException {
		return -1;
	}

	/**
	 * Total amount of time to wait. If less than the wait interval length, only
	 * one attempt will be made
	 * {@link #getRetryInterval(Throwable, IProgressMonitor)}
	 * @return
	 */
	protected long getRetryTimeout() {
		return CloudOperationsConstants.DEFAULT_CF_CLIENT_REQUEST_TIMEOUT;
	}

	public T promptCredentialsAndRun(IProgressMonitor monitor) throws CoreException {
		CloudFoundryServer cloudServer = getCloudServer();

		if (!cloudServer.isSso()) {
			// The username/password should not be null in non-sso scenario.
			if (cloudServer.getUsername() == null || cloudServer.getUsername().length() == 0
					|| cloudServer.getPassword() == null || cloudServer.getPassword().length() == 0) {
				CloudFoundryPlugin.getCallback().getCredentials(cloudServer);
			}
		}
		else {
			if (cloudServer.getToken() == null) {
				CloudFoundryPlugin.getCallback().ssoLoginUserPrompt(cloudServer);
			}
		}

		Server server = (Server) cloudServer.getServer();

		// Any Server request will require the server to be connected, so update
		// the server state
		if (server.getServerState() == IServer.STATE_STOPPED || server.getServerState() == IServer.STATE_STOPPING) {
			server.setServerState(IServer.STATE_STARTING);
		}

		try {

			CFClient client = getClient(monitor);
			if (client == null) {
				throw CloudErrorUtil.toCoreException(NLS.bind(Messages.ERROR_NO_CLIENT, requestLabel));
			}

			httpTrace(client);

			T result = checkClientConnectionAndRun(client, monitor);

			// No errors at this stage, therefore assume operation was completed
			// successfully, and update
			// server state accordingly
			if (server.getServerState() != IServer.STATE_STARTED) {
				server.setServerState(IServer.STATE_STARTED);
			}
			return result;

		}
		catch (CoreException ce) {
			// If the server state was starting and the error is related when
			// the operation was
			// attempted, but the operation failed
			// set the server state back to stopped.
			if (CloudErrorUtil.isConnectionError(ce) && server.getServerState() == IServer.STATE_STARTING) {
				server.setServerState(IServer.STATE_STOPPED);
			}
			// server.setServerPublishState(IServer.PUBLISH_STATE_NONE);
			throw ce;
		}

	}

	private void httpTrace(CFClient client) {
		// TODO: Enable when tracing is available for CFClient wrapper
		// HttpTracer.getCurrent().trace(client);
	}

	protected CFClient getClient(IProgressMonitor monitor) throws CoreException {
		return client;
	}

	protected String generateRequestLabel(String originalLabel, CloudFoundryServer cloudServer) {
		String requestLabel = originalLabel;
		String serverName = null;
		try {
			if (cloudServer != null && cloudServer.getServer() != null) {
				serverName = NLS.bind(Messages.LocalServerRequest_SERVER_LABEL, cloudServer.getServer().getId());
			}
		}
		catch (Throwable e) {
			// Don't log. If the
			// server failed to resolve, the request itself
			// will fail and will log the error accordingly
		}
		if (serverName != null) {
			requestLabel = originalLabel + " - " + serverName; //$NON-NLS-1$
		}

		return requestLabel;
	}

	protected CoreException getErrorOnLastFailedAttempt(Throwable error) {
		if (error instanceof CoreException) {
			return (CoreException) error;
		}
		else {
			return CloudErrorUtil.toCoreException(error);
		}
	}

	protected abstract T runRequest(CFClient client, IProgressMonitor monitor) throws CoreException;

}